﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace Reckoning
{
    /// <summary>
    /// Default implementation of raw word counter
    /// </summary>
    public class Counter: ICounter
    {
        #region ICounter Methods
        public void RunFile(string path,
                            out uint totalWordCount,
                            out Dictionary<string, uint> wordCounts)
        {
            totalWordCount = 0;
            wordCounts = new Dictionary<string, uint>();

            using (StreamReader sr = new StreamReader(path))
            {
                wordCounts.Clear();
                char[] buffer = new char[NUM_CHARS_PER_READ];
                string str = string.Empty;

                while (!sr.EndOfStream)
                {
                    int charsToRead = NUM_CHARS_PER_READ - str.Length;

                    // get enough chars to fill the buffer including the ones left over from last read
                    int numRead = sr.Read(buffer, 0, charsToRead);

                    // concatenate leftovers from last read with this read     
                    str += new string(buffer, 0, numRead);

                    ProcessBuffer(ref str, ref wordCounts);
                }
            }

            totalWordCount = CountTotal(ref wordCounts);
        }

        public void RunText(string text,
                            out uint totalWordCount,
                            out Dictionary<string, uint> wordCounts)
        {
            totalWordCount = 0;
            wordCounts = new Dictionary<string, uint>();

            // append whitespace to the end as a delimiter
            text += ' ';
            ProcessBuffer(ref text, ref wordCounts);

            totalWordCount = CountTotal(ref wordCounts);
        }
        #endregion

        #region Private Properties        
        /// <summary>
        /// Size of text block to read from file
        /// </summary>
        private const int NUM_CHARS_PER_READ = 4096;
        #endregion

        #region Private Methods
        /// <summary>
        /// Process a text block
        /// </summary>
        /// <param name="buffer">text block - any incomplete words will be returned here</param>
        /// <param name="wordCounts">raw word counts</param>
        private void ProcessBuffer(ref string buffer, ref Dictionary<string, uint> wordCounts)
        {
            // lowercase
            buffer = buffer.ToLowerInvariant();

            // replace all non-intwordord punctuation with spaces
            string noPunctuation = RemovePunctuation(buffer);

            // split on whitespace
            string[] split = noPunctuation.Split();

            // increment counts
            int last = split.Count();
            if (!string.IsNullOrEmpty(split[split.Count() - 1]))
            {
                --last;
                buffer = split[split.Count() - 1];
            }
            else
                buffer = string.Empty;

            for (int i = 0; i < last; ++i)
            {
                string s = split[i];

                if (s.Length == 0)
                    continue;

                // see if the token is a word
                bool isWord = false;
                foreach (char c in s)
                {
                    if (char.IsLetterOrDigit(c))
                        isWord = true;
                }

                // don't bother with "words" that contain no letters or digits
                if (!isWord)
                    continue;

                // add to count
                if (wordCounts.ContainsKey(s))
                    ++wordCounts[s];
                else
                    wordCounts[s] = 1;
            }
        }

        /// <summary>
        /// Count total words
        /// </summary>
        /// <param name="wordCounts">raw word counts</param>
        /// <returns>total words</returns>
        private uint CountTotal(ref Dictionary<string, uint> wordCounts)
        {
            uint total = 0;
            foreach (KeyValuePair<string, uint> pair in wordCounts)
                total += pair.Value;

            return total;
        }

        /// <summary>
        /// Replace punctuation chars with space
        /// (except contractional apostrophe and hyphen)
        /// </summary>
        /// <param name="original">original string</param>
        /// <returns>string with replacements</returns>
        private string RemovePunctuation(string original)
        {
            StringBuilder output = new StringBuilder(original.Length);
            for (int i = 0; i < original.Length; ++i)
            {
                char c = original[i];
                bool isPunc = Char.IsPunctuation(c);
                bool isWordPunc = IsWordPunctuation(ref c);
                bool isInterior = IsInWordInterior(original, i);
                bool isNextToSpace = IsNextToSpace(original, i);

                // keep if not punctuation or 
                // if it's in the interior and is word punction
                if (!isPunc || (isInterior && isWordPunc))
                    output.Append(c);

                // replace with space if punctuation but not word punctuation
                // and in the interior
                else if (isPunc && !isNextToSpace)
                    output.Append(' ');

                // anything else is discarded (punctuation at word boundaries)
            }

            return output.ToString();
        }

        /// <summary>
        /// Determine whether this char is one of the types of
        /// punctuation that should be counted as part of the word
        /// if it's in the interior of the word
        /// </summary>
        /// <param name="c">the char</param>
        /// <returns></returns>
        bool IsWordPunctuation(ref char c)
        {
            if (c == '’')
                c = '\'';

            return (c == '\'' || c == '-');
        }

        /// <summary>
        /// Determine if the char at a specified position is in
        /// a word interior (i.e. not at the boundaries)
        /// </summary>
        /// <param name="input">the input string</param>
        /// <param name="index">the position of the char</param>
        /// <returns></returns>
        private bool IsInWordInterior(string input, int index)
        {
            // chars to the left and right must be word chars
            return (index < input.Length - 1 && index > 0 &&
                    Char.IsLetterOrDigit(input[index + 1]) &&
                    Char.IsLetterOrDigit(input[index - 1]));
        }

        private bool IsNextToSpace(string input, int index)
        {
            return (index == 0 ||
                (index < input.Length - 1 && index > 0 &&
                (Char.IsWhiteSpace(input[index + 1]) ||
                Char.IsWhiteSpace(input[index - 1]))));
        }
        #endregion
    }
}
